Reading and Processing JSON Responses in Flutter
JSON (JavaScript Object Notation) is one of the most common formats used by REST APIs to exchange data between a Flutter application and a backend server. In Flutter, JSON responses are usually received as strings and then decoded into Dart objects such as Map, List, and custom model classes.
Flutter provides the dart:convert library for decoding and encoding JSON, while the http package is commonly used to communicate with APIs.
1. What is a JSON Response?
A JSON response is structured data returned by a server after an API request. For example, an API may return the following user information:
{
"id": 101,
"name": "Rahul Sharma",
"email": "[email protected]",
"age": 25,
"isActive": true
}
Flutter receives this response as text and needs to decode it before the application can work with individual fields.
Common JSON Data Types
| JSON Type |
Example |
Dart Representation |
| String |
"Rahul" |
String |
| Number |
25 |
int / double |
| Boolean |
true |
bool |
| Object |
{"id": 1} |
Map |
| Array |
[1, 2, 3] |
List |
| Null |
null |
null |
2. Why JSON Processing is Important in Flutter
- APIs commonly return data in JSON format.
- JSON allows Flutter applications to communicate with backend systems.
- JSON can represent simple values, objects, arrays, and nested structures.
- Decoded JSON can be converted into strongly typed Dart model classes.
- Model classes make API data easier and safer to use throughout an application.
- JSON processing is commonly required for login, registration, products, users, orders, payments, notifications, and other server-based features.
3. JSON Processing Flow in Flutter
A typical API-to-UI flow looks like this:
Flutter App
↓
HTTP Request
↓
Backend API
↓
JSON Response
↓
response.body
↓
jsonDecode()
↓
Map / List
↓
Dart Model Object
↓
Flutter UI
For example:
API Response
↓
"{\"id\":1,\"name\":\"Rahul\"}"
↓
jsonDecode()
↓
Map
↓
User.fromJson()
↓
User object
↓
Text(user.name)
4. Required Packages
For API-based JSON processing, the http package is commonly used.
flutter pub add http
The JSON conversion functions are available in Dart's built-in dart:convert library.
import 'dart:convert';
import 'package:http/http.dart' as http;
5. Understanding response.body
When an HTTP request is completed, the response contains information such as the status code, headers, and response body.
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
print(response.statusCode);
print(response.body);
The response.body value is generally a String containing the server response.
6. Decoding JSON with jsonDecode()
The jsonDecode() function converts a JSON string into Dart data.
import 'dart:convert';
const jsonString = '''
{
"id": 1,
"name": "Rahul",
"email": "[email protected]"
}
''';
final data = jsonDecode(jsonString);
print(data['id']);
print(data['name']);
print(data['email']);
The result of decoding a JSON object is commonly handled as a Map.
7. Converting JSON Object into a Map
A JSON object can be explicitly converted into a Dart map.
final Map userData =
jsonDecode(jsonString) as Map;
print(userData['name']);
This allows individual JSON properties to be accessed using their keys.
8. Reading Individual JSON Fields
final data = jsonDecode(jsonString) as Map;
final int id = data['id'] as int;
final String name = data['name'] as String;
final String email = data['email'] as String;
print(id);
print(name);
print(email);
Why Type Casting is Useful
Type casting helps Dart understand what type of value is expected and provides better type safety.
9. Reading JSON Arrays
APIs often return a list of objects instead of a single object.
[
{
"id": 1,
"name": "Rahul"
},
{
"id": 2,
"name": "Priya"
},
{
"id": 3,
"name": "Amit"
}
]
This response can be decoded into a Dart list:
final List users = jsonDecode(jsonString);
for (final user in users) {
print(user['name']);
}
Converting JSON Array into a List of Maps
final List> users =
(jsonDecode(jsonString) as List)
.cast>();
for (final user in users) {
print(user['name']);
}
10. Reading JSON from an HTTP API
A common process is to send an HTTP request, check the status code, decode the response body, and process the resulting data.
Future fetchUser() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map;
print(data['name']);
print(data['email']);
} else {
throw Exception('Failed to load user');
}
}
11. Always Check the HTTP Status Code
JSON should generally be processed only after determining whether the HTTP request was successful.
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print(data);
} else {
throw Exception(
'Request failed with status ${response.statusCode}',
);
}
| Status Code |
Meaning |
| 200 |
OK |
| 201 |
Created |
| 204 |
No Content |
| 400 |
Bad Request |
| 401 |
Unauthorized |
| 403 |
Forbidden |
| 404 |
Not Found |
| 500 |
Internal Server Error |
| 503 |
Service Unavailable |
12. Creating a Dart Model Class
Instead of working with dynamic maps throughout the application, JSON data can be converted into strongly typed Dart objects.
class User {
final int id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
The model can then be created from the decoded JSON:
final data = jsonDecode(response.body) as Map;
final user = User.fromJson(data);
print(user.name);
print(user.email);
13. Understanding fromJson()
The fromJson() method is commonly used to convert JSON data into a Dart model object.
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
The basic conversion is:
JSON Map
↓
User.fromJson()
↓
User Object
14. Creating a toJson() Method
When Dart objects need to be sent back to an API, they can be converted into JSON-compatible maps.
class User {
final int id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
The object can then be encoded:
final user = User(
id: 1,
name: 'Rahul',
email: '[email protected]',
);
final jsonString = jsonEncode(user.toJson());
print(jsonString);
15. Processing a List of Model Objects
When an API returns multiple users, each JSON object can be converted into a User object.
List parseUsers(String responseBody) {
final List data = jsonDecode(responseBody);
return data
.map(
(item) => User.fromJson(
item as Map,
),
)
.toList();
}
The result is a strongly typed list:
final List users = parseUsers(response.body);
for (final user in users) {
print(user.name);
}
16. Processing Nested JSON
Real-world APIs often contain nested objects.
{
"id": 1,
"name": "Rahul",
"address": {
"city": "Mumbai",
"country": "India"
}
}
Address Model
class Address {
final String city;
final String country;
const Address({
required this.city,
required this.country,
});
factory Address.fromJson(Map json) {
return Address(
city: json['city'] as String,
country: json['country'] as String,
);
}
}
User Model with Nested Address
class User {
final int id;
final String name;
final Address address;
const User({
required this.id,
required this.name,
required this.address,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
address: Address.fromJson(
json['address'] as Map,
),
);
}
}
17. Handling Nullable JSON Fields
Some API fields may contain null. Dart's null safety should be considered when processing such responses.
{
"id": 1,
"name": "Rahul",
"phone": null
}
The model can use a nullable type:
class User {
final int id;
final String name;
final String? phone;
const User({
required this.id,
required this.name,
this.phone,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String?,
);
}
}
18. Providing Default Values
If an API field may be missing or null, a default value can sometimes be used.
final String name = json['name'] as String? ?? 'Unknown User';
Example:
class User {
final String name;
const User({required this.name});
factory User.fromJson(Map json) {
return User(
name: json['name'] as String? ?? 'Unknown User',
);
}
}
19. Handling Boolean Values
{
"id": 1,
"isActive": true
}
final bool isActive = json['isActive'] as bool;
if (isActive) {
print('User is active');
}
20. Handling Numbers
APIs may return integer or decimal values.
{
"id": 10,
"price": 499.99,
"quantity": 3
}
final int quantity = json['quantity'] as int;
final double price = (json['price'] as num).toDouble();
print(quantity);
print(price);
Using num before converting to double can be useful when an API may represent a numeric value as either an integer or decimal.
21. Processing Date Values
JSON does not have a dedicated DateTime type. Dates are commonly returned as strings.
{
"createdAt": "2026-09-19T10:30:00Z"
}
Convert the string into a Dart DateTime:
final DateTime createdAt =
DateTime.parse(json['createdAt'] as String);
print(createdAt);
Inside a model:
class Product {
final int id;
final DateTime createdAt;
const Product({
required this.id,
required this.createdAt,
});
factory Product.fromJson(Map json) {
return Product(
id: json['id'] as int,
createdAt: DateTime.parse(
json['createdAt'] as String,
),
);
}
}
22. Handling JSON Parsing Errors
JSON may be invalid, incomplete, or different from the structure expected by the application. Parsing should therefore be handled carefully.
try {
final data = jsonDecode(response.body);
print(data);
} on FormatException catch (error) {
print('Invalid JSON: $error');
} catch (error) {
print('Unexpected error: $error');
}
Common JSON Parsing Problems
- Null values where a non-null value is expected
- Object received when a list was expected
- List received when an object was expected
- Incorrect nested object structure
23. Object vs List JSON Responses
One common mistake is assuming that every API returns an object.
Object Response
{
"id": 1,
"name": "Rahul"
}
Processing:
final data =
jsonDecode(response.body) as Map;
List Response
[
{
"id": 1,
"name": "Rahul"
},
{
"id": 2,
"name": "Priya"
}
]
Processing:
final data = jsonDecode(response.body) as List;
24. Using JSON Data with FutureBuilder
FutureBuilder can be used to manage asynchronous API responses and display loading, success, and error states.
Future fetchUser() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
);
if (response.statusCode == 200) {
final data =
jsonDecode(response.body) as Map;
return User.fromJson(data);
}
throw Exception('Failed to load user');
}
Use the Future in the widget:
late Future futureUser;
@override
void initState() {
super.initState();
futureUser = fetchUser();
}
Display the result:
FutureBuilder(
future: futureUser,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.hasData) {
final user = snapshot.data!;
return Column(
children: [
Text(user.name),
Text(user.email),
],
);
}
return const Text('No data found');
},
)
25. Complete JSON API Example
The following example demonstrates API request, JSON decoding, model conversion, and displaying the result.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class User {
final int id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
Future fetchUser() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
);
if (response.statusCode == 200) {
final json =
jsonDecode(response.body) as Map;
return User.fromJson(json);
}
throw Exception('Failed to load user');
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('JSON Response Example'),
),
body: FutureBuilder(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (snapshot.hasData) {
final user = snapshot.data!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('ID: ${user.id}'),
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
],
),
);
}
return const Center(
child: Text('No user data available'),
);
},
),
),
);
}
}
26. Processing a JSON List for ListView
When an API returns multiple records, the decoded list can be converted into model objects and displayed using ListView.builder.
Future> fetchUsers() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode != 200) {
throw Exception('Failed to load users');
}
final List data = jsonDecode(response.body);
return data
.map(
(item) => User.fromJson(
item as Map,
),
)
.toList();
}
Display the users:
FutureBuilder>(
future: fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(
child: Text('No users found'),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
},
)
27. Processing JSON for POST Requests
JSON processing is also required when sending data to an API.
Future createUser() async {
final user = {
'name': 'Rahul',
'email': '[email protected]',
};
final response = await http.post(
Uri.parse('https://example.com/api/users'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode(user),
);
if (response.statusCode == 201) {
final data =
jsonDecode(response.body) as Map;
print(data);
} else {
throw Exception('Failed to create user');
}
}
28. Reading API Error JSON
APIs may return structured JSON when an operation fails.
{
"success": false,
"message": "Invalid email address",
"errors": {
"email": "Please enter a valid email"
}
}
The application can read the error message:
if (response.statusCode != 200) {
final errorData =
jsonDecode(response.body) as Map;
final message =
errorData['message'] as String? ?? 'Something went wrong';
throw Exception(message);
}
29. Creating a Reusable JSON Parser
For larger applications, JSON processing logic should be separated from the UI.
class UserService {
Future> fetchUsers() async {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode != 200) {
throw Exception('Failed to load users');
}
final List data = jsonDecode(response.body);
return data
.map(
(item) => User.fromJson(
item as Map,
),
)
.toList();
}
}
The UI can then use the service instead of containing all networking and parsing logic.
30. Separating API, Model, and UI Logic
A clean Flutter application can separate responsibilities into different layers.
lib/
├── models/
│ └── user.dart
├── services/
│ └── user_service.dart
├── screens/
│ └── users_screen.dart
├── widgets/
│ └── user_card.dart
└── main.dart
Responsibilities
| Layer |
Responsibility |
| Model |
Represents and parses application data. |
| Service |
Makes API requests and processes responses. |
| Screen |
Manages the screen-level application state. |
| Widget |
Displays reusable pieces of UI. |
31. Large JSON Responses
Small JSON responses can usually be decoded directly. Very large JSON documents may require additional care because JSON parsing is computational work.
For large responses, parsing can be moved to a background isolate using Flutter's compute() function.
import 'dart:convert';
import 'package:flutter/foundation.dart';
List parseUsers(String responseBody) {
final List data = jsonDecode(responseBody);
return data
.map(
(item) => User.fromJson(
item as Map,
),
)
.toList();
}
Future> fetchUsers() async {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode != 200) {
throw Exception('Failed to load users');
}
return compute(parseUsers, response.body);
}
This approach can help keep expensive JSON parsing work away from the main UI isolate when the response is sufficiently large.
32. Common JSON Processing Mistakes
Mistake 1: Not Checking the Status Code
final data = jsonDecode(response.body);
Do not assume that every HTTP response contains the expected successful JSON structure.
Mistake 2: Calling the API Inside build()
@override
Widget build(BuildContext context) {
fetchUsers();
return const SizedBox();
}
The build() method may execute many times, which can cause repeated requests.
Mistake 3: Using dynamic Everywhere
dynamic user = jsonDecode(response.body);
For larger applications, model classes provide clearer and safer access to API data.
Mistake 4: Ignoring Null Values
final String phone = json['phone'] as String;
If the API returns null, this can fail. Use nullable types or appropriate default values when necessary.
Mistake 5: Assuming the JSON Structure
Always verify whether the API returns an object, list, nested object, or another structure before parsing it.
33. Best Practices for Reading JSON Responses
- Check the HTTP status code before processing the response.
- Use
dart:convert for JSON encoding and decoding.
- Use model classes for structured application data.
- Create
fromJson() methods for JSON-to-object conversion.
- Create
toJson() methods when objects need to be sent to an API.
- Handle nullable fields carefully.
- Handle malformed JSON with appropriate error handling.
- Keep API and parsing logic outside UI widgets when possible.
- Avoid making API calls directly inside
build().
- Use
FutureBuilder or an appropriate state-management solution for asynchronous data.
- Use reusable service classes for API communication.
- Use background parsing for sufficiently large JSON responses.
- Do not expose sensitive API credentials unnecessarily in application code.
34. JSON Serialization vs Deserialization
| Operation |
Meaning |
Example |
| Deserialization |
JSON to Dart object/data |
jsonDecode() |
| Serialization |
Dart object/data to JSON |
jsonEncode() |
JSON String
↓
jsonDecode()
↓
Map / List
↓
Dart Model
Dart Model
↓
toJson()
↓
Map
↓
jsonEncode()
↓
JSON String
35. Manual JSON Parsing vs Code Generation
For small applications and simple models, manual fromJson() and toJson() methods are often sufficient. For larger applications with many models, code-generation approaches such as json_serializable can reduce repetitive serialization code.
Manual Parsing
factory User.fromJson(Map json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
);
}
Advantages of Model-Based Parsing
- Better separation of responsibilities
- More maintainable API integration
36. Practical Example: Product JSON
Suppose an API returns the following product:
{
"id": 101,
"name": "Laptop",
"price": 65000,
"inStock": true
}
Product Model
class Product {
final int id;
final String name;
final double price;
final bool inStock;
const Product({
required this.id,
required this.name,
required this.price,
required this.inStock,
});
factory Product.fromJson(Map json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
inStock: json['inStock'] as bool,
);
}
}
Processing the Response
final data =
jsonDecode(response.body) as Map;
final product = Product.fromJson(data);
print(product.name);
print(product.price);
print(product.inStock);
37. Practical Example: API Response with Data Wrapper
Some APIs wrap the actual records inside a data property.
{
"success": true,
"data": [
{
"id": 1,
"name": "Rahul"
},
{
"id": 2,
"name": "Priya"
}
]
}
The list can be extracted first:
final Map responseData =
jsonDecode(response.body) as Map;
final List data = responseData['data'] as List;
final users = data
.map(
(item) => User.fromJson(
item as Map,
),
)
.toList();
38. JSON Processing Checklist
- Check
response.statusCode.
- Determine whether the result is a Map or List.
- Convert JSON data into Dart model objects.
- Handle null and unexpected values.
- Handle parsing and network errors.
- Pass processed data to the UI.
- Use loading, success, empty, and error states.
39. Practice Exercise
Create a Flutter application that reads a list of products from a JSON API.
- Add the
http package.
- Create a
Product model.
- Add a
Product.fromJson() method.
- Make a GET request.
- Check the HTTP status code.
- Decode the response using
jsonDecode().
- Convert the JSON list into
List.
- Display the products using
ListView.builder.
- Show a loading indicator while the API request is running.
- Show an error message if the request fails.
- Show an appropriate message when the API returns an empty list.
40. Interview Questions
- What is JSON?
- Why is JSON commonly used in Flutter API communication?
- What does
jsonDecode() do?
- What does
jsonEncode() do?
- What is the difference between serialization and deserialization?
- What type of Dart data is commonly produced when decoding a JSON object?
- How do you process a JSON array?
- Why are model classes useful when processing JSON?
- What is the purpose of a
fromJson() method?
- What is the purpose of a
toJson() method?
- How do you process nested JSON?
- How do you handle nullable JSON fields?
- Why should you check
response.statusCode?
- How can JSON parsing errors be handled?
- How can a large JSON response be parsed without causing UI jank?
- Why should API requests generally not be placed directly inside
build()?
- How can JSON data be displayed using
FutureBuilder?
- What is the difference between a JSON object and JSON array?
- When should a service layer be used for API processing?
- What are the advantages of strongly typed model classes?
41. Quick Revision
| Concept |
Purpose |
dart:convert |
Provides JSON encoding and decoding functionality. |
jsonDecode() |
Converts JSON text into Dart data. |
jsonEncode() |
Converts Dart data into JSON text. |
response.body |
Contains the HTTP response body. |
fromJson() |
Converts JSON data into a Dart model. |
toJson() |
Converts a Dart model into JSON-compatible data. |
FutureBuilder |
Builds UI based on asynchronous Future states. |
statusCode |
Indicates the result of an HTTP request. |
compute() |
Can move expensive parsing work to a background isolate. |
42. Useful Resources
Conclusion
Reading and processing JSON responses is an essential skill for Flutter application development. A typical workflow involves making an HTTP request, checking the response status, reading response.body, decoding the JSON using jsonDecode(), converting the decoded data into Dart model objects, handling errors and nullable values, and finally displaying the processed data in the Flutter UI.
For simple applications, manual JSON parsing can be sufficient. As an application grows, strongly typed model classes, service layers, reusable parsing methods, proper error handling, and code-generation solutions can make the code easier to maintain and scale.